查看原文
其他

预训练永不止步,游戏问答语言模型实操

程任清、刘世兴 PaperWeekly 2022-03-17


©PaperWeekly 原创 · 作者|程任清、刘世兴

单位|腾讯游戏知几AI团队

研究方向|自然语言处理




简介


深度学习时代,为了充分训练深层模型参数并防止过拟合,通常需要更多标注数据喂养,在 NLP 领域,标注数据更是一个昂贵资源。在这种背景下,预训练应运而生,该技术可以从大量无标注数据中进行预训使许多 NLP 任务获得显著的性能提升,大批的预训练模型也相继开源了。

但是由于场景的偏差,通用的预训练模型通常无法在垂直领域取得理想的效果,在我们的实际业务中同样也遇到了这个问题, 为了能进一步提升业务下游任务的性能,将大量无标签领域数据价值发挥到最大,我们尝试在游戏问答场景下自研预训练模型 [1],主要遇到的挑战是如何在预训时引入更多的知识。

本文将介绍我们在这方面所做的系列工作,通过这篇文章你将了解到如何快速上手模型预训练,以及如何结合业务知识进一步提升垂直预训的效果。

(我们所有的实验都是在腾讯云智能钛上进行的,感谢为我们提供算力资源!)


我们的模型已开源:

https://share.weiyun.com/S0CeWKGM (UER版)

https://share.weiyun.com/qqJaHcGZ(huggingface版)




基于UER-PY——中文预训练初探


UER-PY 是公司内开源项目 BERT-PyTorch 对外开放的版本,提供了对 BERT [2] 等一系列预训练模型进行了精准的复现,包括预处理、预训练、下游任务微调等步骤,并增加了多项预训练相关的功能和优化。至于我们为什么选用这个框架,一是该项目对中文的支持及一些下游评测的脚本确实挺好用的,二是目前项目在不断更新优化中,遇到问题能得到及时的解决。

上面便是 UER-PY 的简介,感兴趣的同学可以参考 UER-py:


https://github.com/dbiir/UER-py

本小节主要讲述基于 UER-PY 进行 MLM 预训和词粒度预训练的实操。

2.1 MLM预训练


基于 UER-PY 进行预训练,主要步骤如下:

  • 步骤 1:准备训练数据(这里需要注意:语料的格式是一行一个句子,不同文档使用空行分隔,前提是 target 是 bert(包括 mlm 和 nsp,如果 targe 只有 mlm 的话,不需要空行分隔,当时踩了这个坑)
  • 步骤 2:对语料进行预处理
  • 步骤 3:进行预训

具体脚本都可以在官网上找到,我们简单的用自己的业务数据跑了下,并进行了评测。

主要在 CLUE 和游戏领域分类任务上进行了评测。UER-PY 提供了一些挺好用的脚本,方便评测,可以参考 Competition solutions:


https://github.com/dbiir/UER-py/wiki/Competition-solutions

查看其 loss,取的是 100000 steps 的结果。


各任务评测结果:


从上述结果可以看出,使用领域语料进行预训练对于通用的评测任务影响较小,而在业务任务上性能得到提升。除了字粒度,我们也尝试了词粒度的预训,以比较哪种粒度更适合我们的场景。


2.2 词粒度预训练


当我们想要获得特定领域的预训练模型,如果很多词汇不在提供的 vocab.txt 中,这是我们就想根据自己的领域语料来构建自己的词典进行 word-base 预训练。

本节将主要讲述基于游戏领域的语料进行 word-base 的预训练(同样还是在 UER-PY 框架上,并在 Wikizh(word-based)+BertEncoder+BertTarget 基础上进行增量训练,该模型可在 Modelzoo 下载)。

https://github.com/dbiir/UER-py/wiki/Modelzoo

首先利用 jieba 分词进行切词,并在 jieba 自定义词典里加入领域实体词,生成得到分词后的训练数据(all_qa_log.txt),通过脚本构建词典:


#构建领域词典
python3 scripts/build_vocab.py --corpus_path ../data/all_qa_log.txt \
    --vocab_path ../data/zhiji_word_pre/zhiji_word_vocab.txt \
    --workers_num 10 \
    --min_count 30 \ 
    --tokenizer space   


可以调整 min_count 来调整词典大小,min_count 表示一个词在语料中重复出现的次数。我们构建后的词典大小为 137011, 原本词典大小为 80000。

重构词典后,需要相应的调整预训练模型,可通过以下脚本实现:


python3 scripts/dynamic_vocab_adapter.py --old_model_path ../data/wiki_word_model/wiki_bert_word_model.bin \
    --old_vocab_path ../data/wiki_word_model/wiki_word_vocab.txt \
    --new_vocab_path ../data/zhiji_word_pre/zhiji_word_vocab.txt \
    --new_model_path ../data/zhiji_word_pre/zhiji_bert_word_model.bin


这里遇到一个问题:就是词典明明是 137011 大小,调整后的模型 embedded 层词典大小对应是 137010,后查看原因是:构建的词典中有""和" ",uer/utils.vocab.py 中读取代码会将这两个词弄成一个。

#将下行
w = line.strip().split()[0] if line.strip() else " "
#替换为
w = line.strip("\n") if line.strip("\n"else " "

后面便可以准备训练数据和训练啦,脚本可以参考官网,对应的 target 为 mlm,tokenizer 为 space。

通过评测,发现 word-base 预训练效果不太好,猜测可能是由于 OOV 导致的,后来统计了下语料 oov 的词占比为 2.43%,还是挺大的。这里优化的方案是利用词字混合粒度,具体做法如下:

1. 对语料 data.txt(每行一个文本)进行分词 data_cut.txt(每行对应该文本的分词结果,以空格连接);

2. 对 data_cut.txt 进行 space 分词,统计词频,将词频大于 50 的放入词典 vocab_a;

3. 对 data_cut.txt 不在 vocab_a 的词进行字粒度分词,构建新的结果文件 data_cut_cw.txt;

4. 对 data_cut_cw.txt 进行 space 分词,统计词频,将词频大于 50 的词(102846)和 google 的中文词典中的 9614 词(删除了带##的词)和 104 个保留词放入词典 vocab_b(112564)。

通过上面的做法,新构建的词典 oov 占比为 0.03%,解决了 OOV 的问题,然后重新预训后发现效果还是不太好,猜想原因应该还是由于词粒度学习的不够充分,因此后面我们的尝试基本还是基于字粒度展开。

有兴趣的同学可以看看《Is Word Segmentation Necessary for Deep Learning of Chinese Representations》[3] 这篇论文,探讨了中文分词是否必要。




结合领域信息的中文预训练


3.1 全词掩码预训练


Whole Word Masking(wwm),暂翻译为全词 Mask 或整词 Mask,是发布的一项 BERT 的升级版本,主要更改了原预训练阶段的训练样本生成策略。

简单来说,原有基于 WordPiece 的分词方式会把一个完整的词切分成若干个子词,在生成训练样本时,这些被分开的子词会随机被 mask。在整词 Mask 中,如果一个完整的词的部分 WordPiece 子词被 mask,则同属该词的其他部分也会被 mask,即全词 Mask。

同理,由于官方发布的 BERT-base, Chinese 中,中文是以字为粒度进行切分,没有考虑到传统 NLP 中的中文分词(CWS)。将全词 Mask 的方法应用在了中文中,使用了中文维基百科(包括简体和繁体)进行训练,利用分词工具对组成同一个词的汉字全部进行 Mask [4]

以上是 wwm 的简介,更多信息可以参考:


https://github.com/ymcui/Chinese-BERT-wwm 

或《Pre-Training with Whole Word Masking for Chinese BERT》:


https://arxiv.org/abs/1906.08101

本节主要讲述基于游戏语料,利用 huggingface/transformers 在腾讯云智能钛上进行 wwm 预训练实战。(为什么不基于 UER-PY?因为目前它还没不支持 wwm,huggingface 也是最新版本的才支持的,使用过程中也有一些坑,后面会列出来)

3.1.1 准备训练数据


使用分词工具准备 reference file,为什么要这步?直接看下官方的说明

For Chinese models, we need to generate a reference files, because it's tokenized at the character level.
**Q :** Why a reference file?
**A :** Suppose we have a Chinese sentence like: `我喜欢你` The original Chinese-BERT will tokenize it as`['我','喜','欢','你']` (character level). But `喜欢` is a whole word. For whole word masking proxy, we need a result like `['我','喜','##欢','你']`, so we need a reference file to tell the model which position of the BERT original tokenshould be added `##`.

第一步先准备环境,分别是分词功能和 transformers。

分词工具安装有两个问题需要注意 python>3.6以及 3.0=<transformers<=3.4.0,亲测是 python 确实需要 >3.6,而 transformers 不一定。我们使用的 python=3.7.2 和 transformers=3.5.0。


第二步,使用 transformers 源码中的 examples/contrib/run_chinese_ref.py 脚本生成 reference file。

```bash
export TRAIN_FILE=/path/to/dataset/train.txt
export tokens_RESOURCE=/path/to/tokens/tokenizer
export BERT_RESOURCE=/path/to/bert/tokenizer
export SAVE_PATH=/path/to/data/ref.txt
python examples/contrib/run_chinese_ref.py \
    --file_name=path_to_train_or_eval_file \
    --tokens=path_to_tokens_tokenizer \
    --bert=path_to_bert_tokenizer \
    --save_path=path_to_reference_file

分词组件模型可以通过官网下载,另外通过添加自定义词典加入领域实体(我的最大前向分词窗口为 12)。

生成结果为:

#训练数据样例
我喜欢你
兴高采烈表情怎么购买
我需要一张国庆投票券
#对应生成的reference file
[3]
[2, 3, 4, 5, 6, 8, 10]
[3, 7, 8, 9, 10]

解释生成的 reference file:

#以 兴高采烈表情怎么购买 为例
分词后:['兴高采烈表情''怎么''购买']
bert分词后:['[CLS]''兴''高''采''烈''表''情''怎''么''购''买''[SEP]']
reference file对应的为:[23456810] (每个值为对应的bert分词结果的位置索引,并需要和前字进行合并的)
结合bert分词和reference file可以生成:['[CLS]''兴''##高''##采''##烈''##表''##情''怎''##么''购''##买''[SEP]']


3.1.2 进行 wwm 训练


训练的时候,我们用的 transformers 是 3.5.1,脚本用的是该版本的 examples/language-modeling/run_mlm.py。训练参数如下:

data_dir="/home/tione/notebook/data/qa_log_data/"
train_data=$data_dir"qa_log_train.json"
valid_data=$data_dir"qa_log_val.json"
pretrain_model="/home/tione/notebook/data/chinese-bert-wwm-ext"
python -m torch.distributed.launch --nproc_per_node 8 run_mlm_wwm.py \
    --model_name_or_path $pretrain_model \
    --model_type bert \
    --train_file $train_data \
    --do_train \
    --do_eval \
    --eval_steps 10000 \
    --validation_file $valid_data\
    --max_seq_length 64 \
    --pad_to_max_length \
    --num_train_epochs 5.0 \
    --per_device_train_batch_size 128 \
    --gradient_accumulation_steps 16 \
    --save_steps 5000 \
    --preprocessing_num_workers 10 \
    --learning_rate 5e-5 \
    --output_dir ./output_qalog \
    --overwrite_output_dir

对大语料抽样统计了语料中文本长度分布如下图:


我们的训练数据还是短文本居多的,所以最后将 max_seq_length 设为了 64。

模型是在 chinese-bert-wwm-ext 基础上进行增量训练的,chinese-bert-wwm-ext 可以在 huggingface/models 下载:


https://huggingface.co/models

机智如你是不是发现训练的时候并没有用 reference file 啊。是的,因为我们碰到了坑。

3.1.2.1 踩坑:transformers 新版本数据读写依赖了 datasets,这个库带来了什么问题?


这个库依赖的 python 版本 >python3.6,而智能钛预装的环境 python 版本都是 python3.6 的。很简单的想法就是再装一个环境呗。没错,我们也是这样想的,然后装着装着智能钛实例就崩掉了。(内心 yy:前期一直不知道原因,智能钛一旦崩掉可能会由于资源售罄而无法重启,此时心里是慌得一批,我们的数据啊我们的代码啊)。找到原因是智能钛根目录非常小,只有 50G,而 anaconda 默认的安装路径就是在根目录。此时装新环境需要指定目录到 notebook 目录(一般都有 1T)。


#如新建spv环境
conda create --prefix=~/notebook/python_envs/spv
conda activate notebook/python_envs/spv/

然后就可以安装各种你需要的库了。

这个库需要连接外网来获取数据读取脚本(会出现 ConnectionError)。解决的方法是去 datasets 的 git 上下载我们需要的脚本到本地,比如我们需要 text.py 的脚本(即一行一行读取训练数据),在下方链接下载 text.py,并修改 run_mlm_wwm.py 代码。


https://github.com/huggingface/datasets/tree/1.1.2/datasets/text


#将
datasets = load_dataset(extension, data_files=data_files)

#修改为:
datasets = load_dataset("../test.py",data_files=data_files)


3.1.2.2 踩坑:训练数据太大(数据千万级),程序容易被 kill,可以通过下面方法去避免。


一个是在跑 run_mlm_wwm.py 会先将训练数据进行 tokenizer,这个过程会生成很多的cache文件,而这个 cache 文件默认生成的路径是 /home/tione/.cache/huggingface/datasets,这个 /home 是不是很熟悉,就是上面提高的只有 50G 的根目录,所以这一步智能钛也很容易崩,所以需要指定 cache 路径。


#这里增加cache_file_names,制定cache路径。
#这里其实还有一个坑,我们一直以为datasets的是Dataset类,所以指定的参数是cache_file_name,然后它却是DatasetDict,参数为cache_file_names
tokenized_datasets = datasets.map(
        tokenize_function,
        batched=True,
        num_proc=data_args.preprocessing_num_workers,
        remove_columns=[text_column_name],
        load_from_cache_file=not data_args.overwrite_cache,   cache_file_names={"train":"/home/tione/notebook/wwm/process_data/processed_train.arrow","validation":"/home/tione/notebook/wwm/process_data/processed_validation.arrow"},
    )

这一步会生成 cache 文件如下,之后再跑就会优先 load cache 文件,避免重复处理:


在使用 reference file 的时候很容易被 kill 掉。调试的时候会卡到第 5 行,然后就被 kill 了。


def add_chinese_references(dataset, ref_file):
    with open(ref_file, "r", encoding="utf-8"as f:
        refs = [json.loads(line) for line in f.read().splitlines() if (len(line) > 0 and not line.isspace())]
    assert len(dataset) == len(refs)
    dataset_dict = {c: dataset[c] for c in dataset.column_names}
    dataset_dict["chinese_ref"] = refs
    return Dataset.from_dict(dataset_dict)

我们的处理方法是:直接跳过这一步,构建训练数据的时候直接将文本及其对应的 ref 关联好,然后用 Datasets 读入。这也就是为什么上面的训练脚本没有用 reference file。

import json
with open("qa_log_data/qa_log_train.json","w"as fout:
    with open("qa_log_data/qa_log_train.txt","r"as f1:
        with open("qa_log_data/ref_train.txt","r"as f2:
            for x,y in zip(f1.readlines(),f2.readlines()):
                y = json.loads(y.strip("\n"))
                out_dict = {"text":x.strip("\n"),"chinese_ref":y}
                out_dict = json.dumps(out_dict)
                fout.write(out_dict+"\n")

由于训练数据是 json 了,所以上面的 text.py 对应的替换成 json.py(也是在 datasets 的 git 上可以下载到)

3.1.3 wwm评测


在领域分类数据集(这里的测试集和 4.2 的测试集,属于不同时期准备的测试集)上对比的 mlm 模型和 wwm 模型的效果,实验结果如下:


从实验结果来看,wwm 的效果在中文上的表现确实是优于 mlm 预训的结果。


3.2 多任务预训练


本节主要讲述我们领域预训时构建的任务以及对应的实验结果。


3.2.1 任务描述


原始的 WWM 考虑了整词的信息,并且我们通过引入实体词典引入领域实体信息(wwm-ent)。在这基础上我们另外引入了其他信息,我们主要通过下列任务来尝试优化预训练效果:

句子顺序预测(SOP)


ALBERT [5] 指出 BERT 的 NSP 任务其实相当于主题预测和句子的连贯性预测,而随机选择的“下一句”很容易根据主题信息作出正确预测,导致 NSP 任务太简单而效果不明显。

ALBERT 对此提出了“Sentence Order Prediction(SOP)”任务,把相邻的两个文本片段交换顺序作为负例,让模型预测文本片段的顺序,相比 NSP 效果要好。我们的训练数据以短文本为主,缺少文档数据。我们尝试借鉴 SOP 任务,将一个句子分为前后两个部分,正序作为正例,逆序作为负例,预测输入的句子是否是乱序。

替换词检测(RTD)


MLM 任务随机掩盖了部分输入,导致训练阶段和预测阶段输入不一致。于是,我们加入另一个替换词检测训练任务(Replaced Token Detection, RTD)。这个任务是由 ELECTRA [6] 提出的,把输入的部分字用另一个生成器模型的 MLM 预测替换,令判别器模型训练时预测输入的每个字是否是替换过的词。

这个任务相比 MLM 还有一个优点,训练时所有输入字都参与了 loss 的计算,提高了训练效率。和 ELECTRA 不同的是,我们的生成器模型和判别器模型是同一个模型,先进行 MLM 任务的相关计算,得到预测词,然后再用来构造 RTD 任务的输入,让模型预测字是否是替换过。

词语混排(wop)


在文本中,结构知识往往以词语或者句子的顺序来表达,如 sop 通过句子信息来学习到句子结构信息,词语重排序和句子重排目标一致,都是还原原文,迫使模型学习到词法依赖关系。我们实验里主要针对实体词进行了词语的混排预测。


句子对相似度预测(psp)


我们通过句子相似度来引入语义知识,但是一般很难有大量的相似句子的标注语料,考虑我们问答数据知识点的形式,通过语义召回构建了大量正样本和负样本用于学习。


3.2.2 效果对比及分析


下表为我们所做的加入其他任务进行预训的实验结果对比。


结果1:直接利用开源模型进行下游任务评测的结果;

结果2:利用领域数据在开源模型上进行增加训练,效果得到的提升,进一步验证了Don't Stop Pretraining [1]

结果3:我们加入替换词检测任务,实验结果里,加入 RTD 任务的 bert-wwm-rtd 相比 bert-wwm 在领域分类和问答任务任务有了进一步提升;

结果4:加入实体信息的 WWM 相对于没有加入前效果没有达到预期,一个可能的原因是我们加入的实体是通过挖掘得到的,没有经过人工,依赖实体挖掘的效果;

结果5:加入这种类 SOP 任务的方法(bert-wwm-sop)在下游任务上进行评测,效果均略有下降,这个结果和 ALBERT 的实验结果类似,SOP 任务对多句子输入的任务效果更好,我们短文本单句不太适合这类任务;

结果6:我们尝试 wwm、词语混排和句子相似度预测任务进行多任务学习,学习的方式是通过 batch 来循环多任务学习,即 batch1 学习 task1,batch2 学习 task2,batch3 学习 task3,batch4 再学习 task1 的方式避免出现知识遗忘,通过这种方式,效果相比 bert-wwm 在领域分类和问答任务任务有了进一步提升。



总结


本文介绍了基于 UER-py 和腾讯云智能钛如何训练垂直领域预训练语言模型。通过实验多种训练任务和技巧,我们得到了一个在通用领域可用且在游戏问答领域有明显优势的语言模型,我们将模型开源以丰富中文 NLP 社区资源。

我们实践过程中有如下一些发现:

1. 引入词法知识:wwm 在中文上较传统的 mlm 有明显的优势,另外通过  word seg [7,8](类似 pos seg,这里的 word seg:0 代表一个词的开始,1 代表结束)融入词信息的方式并不适合。

2. 引入结构知识:我们通过替换词检测(rtd)和词语混排(wop)任务能很好的引入结构化的知识,而句子顺序预测任务(sop)并不适合我们问答的场景。

3. 引入领域知识:一方面利用问答天然簇形式的数据,构建句子对引入语义知识(psp);另一方面利用实体词 mask 引入更多的词法信息(wwm-ent),相反的直接通过添加 ner 任务引入实体信息效果则不理想。

我们认为,将领域知识嵌入到语言模型中,以及借助语言模型获取领域知识,这两个方向都很重要且相辅相成,在未来一定会取得较大突破。希望感兴趣的同学与我们交流探讨一起合作,更希望志同道合的同学加入我们。



参考文献

[1] Gururangan S, Marasović A, Swayamdipta S, et al. Don't Stop Pretraining: Adapt Language Models to Domains and Tasks[J]. arXiv preprint arXiv:2004.10964, 2020.

[2] Devlin J, Chang M W, Lee K, et al. Bert: Pre-training of deep bidirectional transformers for language understanding[J]. arXiv preprint arXiv:1810.04805, 2018.

[3] Li X, Meng Y, Sun X, et al. Is word segmentation necessary for deep learning of Chinese representations?[J]. arXiv preprint arXiv:1905.05526, 2019.

[4] Cui Y, Che W, Liu T, et al. Pre-training with whole word masking for chinese bert[J]. arXiv preprint arXiv:1906.08101, 2019.

[5] Lan Z, Chen M, Goodman S, et al. A Lite BERT for Self-supervised Learning of Language Representations[J]. arXiv preprint arXiv:1909.11942, 2019.

[6] Clark K, Luong M T, Le Q V, et al. Electra: Pre-training text encoders as discriminators rather than generators[J]. arXiv preprint arXiv:2003.10555, 2020.

[7] Li X, Yan H, Qiu X, et al. Flat: Chinese ner using flat-lattice transformer[J]. arXiv preprint arXiv:2004.11795, 2020.

[8] Liu W, Zhou P, Zhao Z, et al. K-bert: Enabling language representation with knowledge graph[C]//Proceedings of the AAAI Conference on Artificial Intelligence. 2020, 34(03): 2901-2908.



更多阅读




#投 稿 通 道#

 让你的论文被更多人看到 



如何才能让更多的优质内容以更短路径到达读者群体,缩短读者寻找优质内容的成本呢?答案就是:你不认识的人。


总有一些你不认识的人,知道你想知道的东西。PaperWeekly 或许可以成为一座桥梁,促使不同背景、不同方向的学者和学术灵感相互碰撞,迸发出更多的可能性。 


PaperWeekly 鼓励高校实验室或个人,在我们的平台上分享各类优质内容,可以是最新论文解读,也可以是学习心得技术干货。我们的目的只有一个,让知识真正流动起来。


📝 来稿标准:

• 稿件确系个人原创作品,来稿需注明作者个人信息(姓名+学校/工作单位+学历/职位+研究方向) 

• 如果文章并非首发,请在投稿时提醒并附上所有已发布链接 

• PaperWeekly 默认每篇文章都是首发,均会添加“原创”标志


📬 投稿邮箱:

• 投稿邮箱:hr@paperweekly.site 

• 所有文章配图,请单独在附件中发送 

• 请留下即时联系方式(微信或手机),以便我们在编辑发布时和作者沟通



🔍


现在,在「知乎」也能找到我们了

进入知乎首页搜索「PaperWeekly」

点击「关注」订阅我们的专栏吧



关于PaperWeekly


PaperWeekly 是一个推荐、解读、讨论、报道人工智能前沿论文成果的学术平台。如果你研究或从事 AI 领域,欢迎在公众号后台点击「交流群」,小助手将把你带入 PaperWeekly 的交流群里。



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存